﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CkSoftware.GroupMe.Sdk.Core;
using CkSoftware.GroupMe.Sdk.Core.Dao;
using CkSoftware.GroupMe.Sdk.Core.Enums;
using CkSoftware.GroupMe.Sdk.Core.Exceptions;
using CkSoftware.GroupMe.Sdk.Groups.Commands;
using CkSoftware.GroupMe.Sdk.Groups.Dao;
using CkSoftware.GroupMe.Sdk.Messaging;

namespace CkSoftware.GroupMe.Sdk.Groups
{
	/// <summary>
	/// Defines a single GroupMe group containing the group definition and methods to manage users and messages.
	/// </summary>
	public class GroupMeGroup
	{
		private const int AddMemberPollingDelayInMs = 500;
		private readonly GroupData _groupData;
		private readonly Lazy<GroupMeMessages> _groupMessages;

		private readonly Dictionary<Guid, IEnumerable<AddMemberData>> _runningAddMemberTasks;
		private readonly IWebApiAccess _webApiAccess;
		private GroupAddMemberCommand _currentAddMemberCommand;

		/// <summary>
		/// Instantiates a new <see cref="GroupMeGroup" /> object.
		/// </summary>
		/// <param name="webApiAccess">The <see cref="IWebApiAccess" /> which should be used to call the API.</param>
		/// <param name="groupData">The already accessed <see cref="GroupData"/> for the current group.</param>
		public GroupMeGroup(IWebApiAccess webApiAccess, GroupData groupData)
		{
			_webApiAccess = webApiAccess;
			_groupData = groupData;
			_runningAddMemberTasks = new Dictionary<Guid, IEnumerable<AddMemberData>>();
			_groupMessages = new Lazy<GroupMeMessages>(() => new GroupMeGroupMessages(_webApiAccess, _groupData.Id));
		}

		/// <summary>
		/// The current <see cref="GroupData"/>.
		/// </summary>
		public GroupData Data
		{
			get { return _groupData; }
		}

		/// <summary>
		/// The <see cref="GroupMeMessages"/> object which manages the messages for this group.
		/// </summary>
		public GroupMeMessages Messages
		{
			get { return _groupMessages.Value; }
		}

		/// <summary>
		/// Adds the given member-data to a list of members to add to the group. This DOES NOT directly submit the member to the
		/// group. To submit the added member(s), call SubmitAddedMembers().
		/// </summary>
		/// <param name="nickname">The member's nickname.</param>
		/// <param name="userId">The user-id of the member to add. At least one of userId, phoneNumber or email must be specified.</param>
		/// <param name="phoneNumber">The phoneNumber of the member to add.</param>
		/// <param name="email">The email of the member to add.</param>
		/// <returns>Reference to this, used for FluentApi calls.</returns>
		public GroupMeGroup AddMember(string nickname, string userId = "", string phoneNumber = "", string email = "")
		{
			if (_currentAddMemberCommand == null)
			{
				_currentAddMemberCommand = new GroupAddMemberCommand(Data.Id);
			}

			_currentAddMemberCommand.WithMember(nickname, userId, phoneNumber, email);

			return this;
		}

		/// <summary>
		/// Checks the status for the given results id and returns the status including the finished and missing member-data.
		/// </summary>
		/// <param name="resultsId">The results id from the SubmitAddedMembersAsync() call.</param>
		/// <returns>The AddedMember Results data.</returns>
		public async Task<GroupAddMemberResultsData> CheckAddMemberResults(Guid resultsId)
		{
			var command = new GroupAddMemberResultsCommand(Data.Id, resultsId);
			GroupMeApiResponse<GroupAddMemberResultsData> results = await _webApiAccess.ExecuteCommand(command);

			GroupAddMemberResultsData resultsData = results.Response ?? new GroupAddMemberResultsData();

			switch (results.Meta.ParsedCode)
			{
				case GroupMeApiResultCode.Ok:
					resultsData.Status = AsyncGroupMeApiStatus.OperationDone;

					if (_runningAddMemberTasks.ContainsKey(resultsId))
					{
						resultsData.MissingMembers = SortOutMissingMembers(resultsData.AddedMembers, _runningAddMemberTasks[resultsId]);
						_runningAddMemberTasks.Remove(resultsId);
					}
					break;
				case GroupMeApiResultCode.ServiceUnavailable:
					resultsData.Status = AsyncGroupMeApiStatus.ResultsNotYetReady;
					break;
				case GroupMeApiResultCode.NotFound:
					resultsData.Status = AsyncGroupMeApiStatus.ResultsNoLongerAvailable;
					break;
				default:
					throw new GroupMeException(results.Meta);
			}

			return resultsData;
		}

		/// <summary>
		/// Removes a user from this group.
		/// </summary>
		/// <param name="groupMemberId">
		/// The member Id to remove. Needs to be the group-member-id from the field Id of the
		/// <see cref="GroupMemberData" />.
		/// </param>
		/// <returns>The async Task to wait for.</returns>
		public async Task RemoveMember(string groupMemberId)
		{
			var command = new GroupRemoveMemberCommand(Data.Id, groupMemberId);
			await _webApiAccess.ExecuteCommand(command);
		}

		/// <summary>
		/// Removes a user from this group.
		/// </summary>
		/// <param name="member">The member to remove.</param>
		/// <returns>The async Task to wait for.</returns>
		public async Task RemoveMember(GroupMemberData member)
		{
			var command = new GroupRemoveMemberCommand(Data.Id, member.Id);
			await _webApiAccess.ExecuteCommand(command);
		}

		/// <summary>
		/// Resolves all likes of the given message to their respective group members.
		/// </summary>
		/// <param name="message">The message to resolve the likes.</param>
		/// <returns>
		/// The Group members who liked the message. If the member is not found in the current group-data, then this
		/// member will be omited.
		/// </returns>
		public IEnumerable<GroupMemberData> ResolveLikesForMessage(GroupMeMessageData message)
		{
			return message.LikedByUserIds
				.Select(liker => _groupData.Members.FirstOrDefault(member => member.UserId == liker))
				.Where(memberData => memberData != null);
		}

		/// <summary>
		/// Submits the previously prepared AddMembers Command, awaits the results and returns the AddMember results.
		/// </summary>
		/// <returns>The results of the AddMembers command.</returns>
		public async Task<GroupAddMemberResultsData> SubmitAddedMembersAndWaitForResults()
		{
			Guid resultsId = await SubmitAddedMembersAsync();
			return await PollAddMemberResultsAndGetAddedMembers(resultsId);
		}

		/// <summary>
		/// Submits the previously prepared AddMembers Command and returns the Results Id required for Result-Polling / Checking.
		/// </summary>
		/// <exception cref="GroupMeException">Occurs if you try to execute the AddMembers without actually adding any members.</exception>
		/// <returns>The ResultsId used for Result-Polling / Checking if the results are ready.</returns>
		public async Task<Guid> SubmitAddedMembersAsync()
		{
			if (_currentAddMemberCommand == null)
			{
				throw new GroupMeException(new MetaData
				{
					Code = 500,
					Errors = new[] {"You cannot submit the AddMembers command if you have not added any members yet."}
				});
			}

			IEnumerable<AddMemberData> addedMembers = _currentAddMemberCommand.AddedMembers;
			Guid resultsId = await ExecuteAddMemberCommandAndGetResultsGuid();

			_runningAddMemberTasks.Add(resultsId, addedMembers);

			return resultsId;
		}

		private async Task<Guid> ExecuteAddMemberCommandAndGetResultsGuid()
		{
			GroupMeApiResponse<AsyncGroupMeApiResult> result = await _webApiAccess.ExecuteCommand(_currentAddMemberCommand);

			if (result.Meta.ParsedCode != GroupMeApiResultCode.Accepted)
			{
				throw new GroupMeException(result.Meta);
			}

			_currentAddMemberCommand = null;

			return result.Response.ResultsGuid;
		}

		private async Task<GroupAddMemberResultsData> PollAddMemberResultsAndGetAddedMembers(Guid resultsId)
		{
			GroupAddMemberResultsData results = await CheckAddMemberResults(resultsId);

			while (results.Status != AsyncGroupMeApiStatus.OperationDone &&
			       results.Status != AsyncGroupMeApiStatus.ResultsNoLongerAvailable)
			{
				await Task.Delay(AddMemberPollingDelayInMs);
				results = await CheckAddMemberResults(resultsId);
			}

			return results;
		}

		private IEnumerable<GroupMemberAddedMissingData> SortOutMissingMembers(IEnumerable<GroupMemberAddedData> addedMembers,
			IEnumerable<AddMemberData>
				originallyAddedMembers)
		{
			return originallyAddedMembers
				.Where(originalAddedMember => !addedMembers.Any(member => member.Guid.Equals(originalAddedMember.Guid)))
				.Select(originalAddedMember => new GroupMemberAddedMissingData(originalAddedMember));
		}
	}
}